package com.small.nine.watch.dog.common.lock;

import cn.hutool.core.util.StrUtil;
import cn.hutool.extra.spring.SpringUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.ValueOperations;

import java.util.Objects;
import java.util.concurrent.Executor;
import java.util.concurrent.SynchronousQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Condition;
import java.util.concurrent.locks.Lock;

/**
 * 看门狗机制 锁
 *
 * @author sheng_zs@126.com
 * @date 2021-08-09 11:19
 */
@Slf4j
public class WatchDogLock implements Lock {
    /**
     * redis 操作
     */
    private final RedisTemplate<String, Object> REDIS_TEMPLATE = SpringUtil.getBean("redisTemplate");

    /**
     * redis String 操作
     */
    private final ValueOperations<String, Object> VALUE_OPERATIONS = SpringUtil.getBean("valueOperations");

    /**
     * 线程池，SynchronousQueue：无缓冲等待队列，直接把任务交给消费者，必须等队列中的任务被消费才可以继续添加任务
     * 为避免线程池采取拒绝策略，一般设置 maximumPoolSize 为 Integer,MAX_VALUE
     */
    private final Executor EXECUTOR = new ThreadPoolExecutor(10, Integer.MAX_VALUE, 5L, TimeUnit.MINUTES, new SynchronousQueue<Runnable>());

    /**
     * redis key
     */
    private final String key;

    /**
     * 看门狗线程
     */
    private final WatchDogThread watchDogThread;

    /**
     * 是否已经加锁
     */
    private boolean isLock;

    public WatchDogLock(String key) {
        if (StrUtil.isBlank(key)) {
            throw new RuntimeException("key 不能为空");
        }
        this.key = key;
        this.watchDogThread = new WatchDogThread();
    }

    @Override
    public void lock() {
        // 无限期重试获取锁，并开启看门狗机制
        lock(true, null, null, true);
    }

    @Override
    public void lockInterruptibly() throws InterruptedException {
        // 无限期重试获取锁
        lock(true, null, null, false);
    }

    /**
     * 加锁
     *
     * @param reTry     是否重试获取锁
     * @param time      重试获取锁时间
     * @param unit      重试获取时间的时间单位
     * @param openWatch 是否开启看门狗机制
     * @return 加锁成功返回 true
     */
    private boolean lock(boolean reTry, Long time, TimeUnit unit, boolean openWatch) {
        // 开始时间，以及 重试获取锁的时间
        long start = 0L, tryTime = 0L;
        // 是否有 重试时间
        boolean hasTryTime;
        if (hasTryTime = Objects.nonNull(time) && Objects.nonNull(unit)) {
            start = System.currentTimeMillis();
            tryTime = unit.toMillis(time);
        }
        // 设置到 redis 是否成功
        Boolean flag;
        log.info("尝试获取锁");
        do {
            flag = VALUE_OPERATIONS.setIfAbsent(key, true, 30L, TimeUnit.SECONDS);
            // 当 重试 && 设置不成功 && （没有重试时间 || 在重试时间内）
        } while (reTry && !Objects.equals(flag, true) && (!hasTryTime || System.currentTimeMillis() - start < tryTime));
        // 获得锁后，开启看门狗线程，续命
        boolean success;
        if (success = Objects.equals(flag, true)) {
            // 已经加锁
            this.isLock = true;
            if (openWatch) {
                EXECUTOR.execute(watchDogThread);
            }
        }
        return success;
    }

    @Override
    public boolean tryLock() {
        // 不重试获取，如果获取锁成功，则开启看门狗机制
        return lock(false, null, null, true);
    }

    @Override
    public boolean tryLock(long time, TimeUnit unit) {
        // time unit 时间内重试获取锁，如果获取成功，则开启看门狗机制
        return lock(true, time, unit, true);
    }

    @Override
    public void unlock() {
        if (!isLock) {
            throw new RuntimeException("还未加锁");
        }

        log.error("看门狗 set false");
        watchDogThread.atomic.set(false);

        REDIS_TEMPLATE.delete(key);
        log.error("锁已释放");
    }

    @Override
    public Condition newCondition() {
        throw new RuntimeException("不可创建 Condition");
    }

    /**
     * 看门狗线程
     */
    class WatchDogThread implements Runnable {
        private final AtomicBoolean atomic = new AtomicBoolean(true);

        @Override
        public void run() {
            /*
                休眠 10 秒，续命 10 秒
             */
            while (atomic.compareAndSet(true, true)) {
                try {
                    TimeUnit.SECONDS.sleep(10);
                } catch (InterruptedException e) {
                    log.error("线程睡眠异常");
                    return;
                }
                log.error("看门狗线程-续命");

                // 续命 10 秒
                REDIS_TEMPLATE.expire(key, 30, TimeUnit.SECONDS);
            }
        }
    }
}
